#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" This is the main library for tack.


"""
from cgi import escape
from cStringIO import StringIO
from functools import partial
from string import Template
from wsgiref.util import setup_testing_defaults, shift_path_info

from tack.lib.jsontools import json_dumps


def compose(fs):
    """ compose(funcions) -> F(G(...))

    Compose a function from a sequence of functions, from left to
    right.

    >>> def F(x):
    ...     return x+1
    ... 
    >>> def G(x):
    ...     return x+2
    ... 
    >>> compose((F, G))
    <function composed at 0x...>
    >>> compose((F, G))(0)
    3
    >>> compose((F, G))(10)
    13
    """
    f, fs = fs[-1], reversed(fs[:-1])
    def composed(*args, **kwargs):
        val = f(*args, **kwargs)
        for fn in fs:
            val = fn(val)
        return val
    return composed


def import_name(name):
    """ Import module via dotted string notation.

    Copied from python library reference section 2.1

    >>> m = import_name('tack.lib')
    >>> m
    <module 'tack.lib' from ...>
    """
    mod = __import__(name)
    parts = name.split('.')
    for part in parts[1:]:
        mod = getattr(mod, part)
    return mod


def import_attr(name):
    """ Import value from module via dotted string notation.

    >>> app = import_attr('tack.lib.fileserver.fileserver')
    >>> app
    <function fileserver at 0x...>
    """
    names = name.split('.')
    mod = str.join('.', names[0:-1])
    name = names[-1]
    return getattr(import_name(mod), name)


def copy_appattrs(source, target, func_original=False):
    """ Copy function name and doc from source to target.  Returns target.

    @param source function or object with 'func_name' and 'func_name' attribs
    @param target function or object
    @param func_original=False if true, saves ref to source at target.func_original
    @return target

    >>> def src():
    ...     'src doc string'
    >>> def dst():
    ...     'dst doc string'
    >>> new = copy_appattrs(src, dst)
    >>> new
    <function src at 0x...>

    >>> new.func_name
    'src'

    >>> new.__doc__ == src.__doc__
    True

    """
    try:
        target.func_name = source.func_name
        target.func_doc = source.func_doc
        if func_original:
            target.func_original = source
    except AttributeError:
        raise
    return target


def response_replacement():
    """

    >>> r, s = response_replacement()
    >>> r
    ['', [], None]

    >>> s
    <function capture_start_response at 0x...>

    >>> def app(env, respond):
    ...     respond('999 - Test Response', [('key', 'value')])
    >>> app({}, s)
    >>> r
    ['999 - Test Response', [('key', 'value')], None]

    """
    response = ['', [], None]
    def capture_start_response(status, headers, exc_info=None):
        response[0] = status
        response[1].extend(headers)
        response[2] = exc_info
    return response, capture_start_response


def default_env(overrides={}, **kwds):
    """


    >>> env = default_env()
    >>> 'HTTP_HOST' in env
    True

    >>> 'PATH_INFO' in env
    True

    >>> 'QUERY_STRING' in env
    True

    >>> env['wsgi.errors']
    <StringIO.StringIO instance at 0x...>

    >>> env['wsgi.input']
    <StringIO.StringIO instance at 0x...>
    """
    env = dict(QUERY_STRING='')
    setup_testing_defaults(env)
    env.update(overrides)
    env.update(kwds)
    return env


def default_responder(*a, **b):
    pass


def run_app(app, env=(), **kwds):
    response, start_response = response_replacement()
    environ = default_env()
    environ.update(env)
    environ.update(kwds)
    result = app(environ, start_response)
    return response, result


class status_codes:
    okay = '200 OK'
    created = '201 Created'
    moved_perm = '301 Moved Permanently'
    bad_request = '400 Bad Request'
    unauthorized = '401 Unauthorized'
    forbidden = '403 Forbidden'
    not_found = '404 Not Found'
    bad_method = '405 Method Not Allowed'
    conflict = '409 Conflict'
    error = '500 Server Error'


class content_headers:
    """

    >>> content_headers
    <class tack.lib.content_headers at 0x...>

    >>> c = content_headers()
    >>> c
    <tack.lib.content_headers instance at 0x...>
    """
    content_type = 'Content-Type'
    text_html = (content_type, 'text/html')
    text_js = (content_type, 'text/javascript')
    text_plain = (content_type, 'text/plain')
    text_xhtml = (content_type, 'text/xhtml')
    text_xml = (content_type, 'text/xml')

    cache_control = 'Cache-Control'
    cache_none = [(cache_control, 'no-cache')]
    
    ## TODO:  make default_plain and friends simple lists
    @classmethod
    def default_plain(cls):
        """

        >>> content_headers.default_plain()
        [('Content-Type', 'text/plain')]

        >>> content_headers().default_plain()
        [('Content-Type', 'text/plain')]
        """
        return [cls.text_plain]

    @classmethod
    def redirect_plain(cls, location):
        return [cls.text_plain, ('Location', location)]

    @staticmethod
    def length(value):
        """

        >>> content_headers.length('example.com')
        ('Content-Length', '11')

        >>> content_headers().length('example.com')
        ('Content-Length', '11')
        """
        try:
            value = len(value)
        except (TypeError, ):
            pass
        return ('Content-Length', str(value))


def extra_headers_app(app, headers):
    def extra_headers(environ, start_response):
        state, responder = response_replacement()
        results = app(environ, responder)
        state[1].extend(headers)
        start_response(*state)
        return results
    return extra_headers


no_cache_app = partial(extra_headers_app, headers=content_headers.cache_none)


def string_app(output,
               status=status_codes.okay,
               content_type=content_headers.text_plain,
               extra_headers=[],
               formatter=str,
               name=''):
    """

    >>> app = string_app("this is a test")
    >>> app({}, lambda c, h:None)
    ['this is a test']

    """
    output = formatter(output)
    headers = [content_type, content_headers.length(output)]
    def app(environ, start_response):
        start_response(status, headers + extra_headers)
        return [output]
    if name:
        app.func_name = name
    return app


default401 = string_app('401 Unauthorized',
                        status=status_codes.unauthorized,
                        extra_headers=[('WWW-Authenticate', 'Basic realm=')])
default403 = string_app('403 Forbidden',
                        status=status_codes.forbidden)
default500 = string_app('500 Server Error',
                        status=status_codes.error)
error_string_app = partial(string_app, status=status_codes.error)
badrequest_string_app = partial(string_app, status=status_codes.bad_request)
html_string_app = partial(string_app, content_type=content_headers.text_html)
json_app = partial(string_app, formatter=partial(json_dumps, indent=2))


class env_keys:
    http_auth = 'HTTP_AUTHORIZATION'
    http_cookie = 'HTTP_COOKIE'
    remote_user = 'REMOTE_USER'


def func_app(call,
            status=status_codes.okay,
            content_type=content_headers.text_plain):
    """

    >>> app = func_app(lambda:"this is a test")
    >>> app({}, lambda c, h:None)
    ['this is a test']

    """
    def app(environ, start_response):
        strapp = string_app(call(), status, content_type)
        return strapp(environ, start_response)
    return app


def echo_app(*keys):
    """

    Test getting one

    >>> app = echo_app('HTTP_HOST')
    >>> run_app(app, default_env())
    (['200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '24')], None], ["HTTP_HOST = '127.0.0.1'\\n"])

    Testing getting them all

    >>> app = echo_app()
    >>> result = run_app(app, default_env())
    >>> values = result[1][0].split('\\n')
    >>> env = {}
    >>> for line in [ line for line in values if line ]:
    ...     (k, v) = line.split(' = ')
    ...     env[k] = v
    >>> 'HTTP_HOST' in env
    True

    >>> 'SERVER_PORT' in env
    True

    >>> 'wsgi.version' in env
    True
    """
    def app(environ, start_response):
        stdout = StringIO()
        items = [(k, environ.get(k)) for k in keys] if keys else environ.items()
        for item in sorted(items):
            print >> stdout, '%s = %r' % item
        return string_app(stdout.getvalue())(environ, start_response)
    return app


class status_doc:
    template = '<html><head><title>%s</title></head><body>%s</body></html>\n'
    not_found = template % (status_codes.not_found, status_codes.not_found)
    not_allowed = template % (status_codes.bad_method, status_codes.bad_method)
    forbidden = template % (status_codes.forbidden, status_codes.forbidden)


def not_found(environ, start_response):
    """

    >>> example_start_response = lambda *a, **b:None
    >>> result = not_found(default_env(), example_start_response)

    """
    start_response(status_codes.not_found, [content_headers.text_html])
    return [status_doc.not_found % environ]


def not_allowed(environ, start_response):
    """

    >>> example_start_response = lambda *a, **b:None
    >>> result = not_found(default_env(), example_start_response)

    """
    start_response(status_codes.bad_method, [content_headers.text_html])
    return [status_doc.not_allowed]


## TODO: need shift_l and shift_r versions.
def shift_app(prefix, app):
    """ Creates app that shifts PATH_INFO.

    >>> s = shift_app('any', None)

    >>> s.__name__
    'shifter'

    >>> callable(s)
    True
    """
    def shifter(env, resp):
        pathinfo = env['PATH_INFO']
        if pathinfo.startswith(prefix):
            shift_path_info(env)
        return app(env, resp)
    return shifter


def template_app(filename, handle500=default500, **kwds):
    def esc(pairs):
        return [(escape(str(k)), escape(str(v))) for k, v in pairs]

    def template_inner(environ, start_response):
        mapping = dict(esc(environ.items()) + esc(kwds.items()))
        try:
            output = Template(open(filename).read()).safe_substitute(mapping)
        except:
            return handle500(environ, start_response)
        return html_string_app(output, **kwds)(environ, start_response)
    return template_inner
